package org.stephen.recommend.recommender;

import org.stephen.recommend.algorithms.AbstractRecommender;
import org.stephen.recommend.entity.Article;
import org.stephen.recommend.enums.LanguageType;
import org.stephen.recommend.model.*;
import org.stephen.recommend.service.CmsArticleService;
import org.stephen.recommend.thread.AsyncService;
import org.stephen.recommend.util.CommonUtils;
import org.stephen.recommend.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.stream.Collectors;

/**
 * ClassName: MahoutUserBasedCollaborativeRecommender
 * Description: 基于用户的协同过滤
 * Date: 2020/12/8 20:37
 *
 * @author stephen.qiu
 * @version 1.0
 */
@Component
public class MahoutUserBasedCollaborativeRecommender extends AbstractRecommender {
    private static final Logger logger = LoggerFactory.getLogger(MahoutUserBasedCollaborativeRecommender.class);

    @Autowired
    CmsArticleService cmsArticleService;

    @Autowired
    AsyncService asyncService;

    /**
     * 针对特定用户返回推荐结果
     *
     * @param blockArticleIds 用户屏蔽的文章
     * @param userId          用户id/设备id
     * @param languageType    语言环境
     */
    @Override
    public void recommend(List<String> blockArticleIds, String userId, LanguageType languageType) {
        logger.info("----------开始基于用户的协同过滤推荐----------");
        /**
         * 获取等待推荐的文章,不够总推荐数量再使用协同推荐
         */
        String recommendArticleIds = getRecommendArticleIds(userId);
        if (recommendArticleIds == null || recommendArticleIds.split(",").length < propGetKit.getTotalRecNum()) {
            /**
             * 获取用户浏览历史
             */
            ViewedArticles viewedArticles = getViewedArticles(userId);
            if (viewedArticles == null) {
                /**
                 * 用户没有浏览历史，无法进行基于用户的协同过滤推荐
                 */
                logger.info("CF缺乏用户行为数据,无法进行协同过滤推荐");
                return;
            } else if (viewedArticles.getViewedArticleTimeMap().size() == 0) {
                logger.info("用户行为数据已经过期,无法进行内容推荐");
                return;
            }
            /**
             * 获取相似用户列表
             */
            SimilarityUsers similarityUsers = getSimilarityUsers(userId);
            if (similarityUsers != null && similarityUsers.getSimilarityUserTimeMap().size() != 0) {
                /**
                 * 获取所有不会被推荐的文章列表
                 */
                Set<String> allBlockArticleIdsSet = getAllBlockArticleIdsSet(userId, blockArticleIds);
                /**
                 * 获取相似用户浏览过的文章,排除所有不会被推荐的文章,排除时效天数之外的文章。
                 */
                List<String> similarityUsersArticles = new LinkedList<>();
                List<String> similarityUsersList = similarityUsers.getSimilarityUsersList();
                similarityUsersList.forEach(similarityUser -> {
                    ViewedArticles similarityUserViewedArticles = getViewedArticles(similarityUser);
                    if (similarityUserViewedArticles != null) {
                        List<String> articleIds = similarityUserViewedArticles.getViewedArticlesList(propGetKit.getCFValidDay());
                        articleIds.forEach(articleId -> {
                            if (Utils.isEmpty(allBlockArticleIdsSet) || !allBlockArticleIdsSet.contains(articleId)) {
                                similarityUsersArticles.add(articleId);
                            }
                        });
                    }
                });
                /**
                 * 获取和用户标签最符合的文章
                 */
                if (similarityUsersArticles.size() > 0) {
                    /**
                     * 获取用户浏览历史标签排序
                     */
                    UserTags userTags = getUserTags(userId, languageType);
                    if (userTags == null) {
                        logger.info("CF用户标签尚未生成");
                        return;
                    } else if (userTags.getUserTagPointMap().size() == 0) {
                        logger.info("CF用户标签已经过期");
                        return;
                    }
                    List<String> mahoutUserBaseRecommendArticleIds = getRecommendArticle(userTags.getUserTagPointMap(), similarityUsersArticles, propGetKit.getCFRecNum(), languageType);

                    logger.info("CF协同推荐文章数量:" + mahoutUserBaseRecommendArticleIds.size());

                    setRecommendArticles(mahoutUserBaseRecommendArticleIds, userId);
                } else {
                    logger.info("CF协同推荐文章数量:" + 0);
                }
            } else {
                /**
                 * 没有相似用户，异步寻找相似用户，协同过滤推荐结束。
                 */
                logger.info("没有相似用户，异步寻找相似用户，协同过滤推荐结束。");
                logger.info("CF协同推荐文章数量:" + 0);
                return;
            }
        }
    }

    /**
     * Description: 获取基于用户协同过滤的文章
     * 获取被推荐者的爱好标签map,Map<String, Double> tagList,Double对应的是该标签的权重/得分，
     * 遍历相似用户所浏览过的文章，获取文章所对应的标签，遍历该标签列表，判断这些标签是否在被推荐者的爱好标签map内，
     * 如果属于被推荐者的爱好标签，该标签的权重/得分会添加进该文章内，Map<String, Double> Double对应的是该文章的权重/得分，
     * 最后对map进行升序排序，取出前num个文章作为推荐文章。
     *
     * @param tagList      被推荐者的喜好标签列表
     * @param articleIds   和被推荐者相似用户所浏览过的文章id列表
     * @param num          获取数量
     * @param languageType 文章语言类型过滤
     * @return
     */
    public List<String> getRecommendArticle(Map<String, Double> tagList, List<String> articleIds, int num, LanguageType languageType) {
        Map<String, Double> map = new HashMap<>(CommonUtils.initHashMapSize(articleIds.size()));
        for (String articleId : articleIds) {
            if (HotNewsList.getInstance().isExist(articleId, languageType)) {
                Article article = cmsArticleService.getById(articleId);
                if (Utils.isEmpty(article.getTag())) {
                    continue;
                }
                String[] tags = article.getTag().split(",");
                Double point = 0.0;
                for (String tag : tags) {
                    point += (1.0 + tagList.getOrDefault(tag.split(":")[0], 0.0)) * (1.0 + Double.valueOf(tag.split(":")[1]));
                }
                map.put(articleId, point);
            }
        }
        List<Map.Entry<String, Double>> articleList = new ArrayList<>(map.entrySet());
        Collections.sort(articleList, CommonUtils.getComparator());
        articleList = articleList.size() <= num ? articleList : articleList.subList(0, num);
        return articleList.stream().map(c -> c.getKey()).collect(Collectors.toList());
    }
}
